using System;
using System.Reflection;

namespace Meta {
	/// <summary>
	/// MemberAccessors handle member accesses in compiled and interpreted code.
	/// They cache member lookups for speed.
	/// </summary>
	/// 
	[Serializable]
    [System.Diagnostics.DebuggerDisplay("{memberName}")]
	public class MemberAccessor {
		string memberName;
		MemberInfo member;
		enum Mode {
			Initialize, Field, Property, Event, Method, AmbiguousMethod,
			StaticField, StaticProperty, StaticEvent, StaticMethod, AmbiguousStaticMethod,
			OpenField, OpenProperty, OpenEvent, OpenMethod,
			SubType
		};
		Mode mode;

		public MemberAccessor(string memberName) {
			this.memberName = memberName;
		}

		private void LookupMemberFor(object o) {
			// Look for a member by the name memberName
			MemberInfo[] matches = o.GetType().GetMember(memberName, BindingFlags.Public | BindingFlags.NonPublic | BindingFlags.Instance | BindingFlags.Static);
			if (matches == null || matches.Length==0) {
				// Didn't find it.  Check if it's a static member.
				if (o is Type) {
					MemberInfo[] smatches = ((Type)o).GetMember(memberName, BindingFlags.Public | BindingFlags.NonPublic | BindingFlags.Instance | BindingFlags.Static);
					if (smatches == null || smatches.Length==0)
						throw new UnknownMemberException(o, memberName);
					member = smatches[0];
					if (smatches.Length==1) {
						if (member is FieldInfo)
							mode = ((((FieldInfo)member).Attributes&FieldAttributes.Static)!=0)?Mode.StaticField:Mode.OpenField;
						else if (member is PropertyInfo) {
							MethodInfo getter = ((PropertyInfo)member).GetGetMethod(true);
							mode = ((getter.Attributes&MethodAttributes.Static)!=0)?Mode.StaticProperty:Mode.OpenProperty;
						} else if (member is EventInfo) {
							MethodInfo raiser = ((EventInfo)member).GetRaiseMethod();
							mode = ((raiser.Attributes&MethodAttributes.Static)!=0)?Mode.StaticEvent:Mode.OpenEvent;
						} else if (member is MethodInfo)
							mode = ((((MethodInfo)member).Attributes&MethodAttributes.Static)!=0)?Mode.StaticMethod:Mode.OpenMethod;
						else if (member is Type)
							mode = Mode.SubType;
					} else
						mode = Mode.AmbiguousStaticMethod;
				} else
					throw new UnknownMemberException(o, memberName);
			} else {
				// It's an instance member.
				member = matches[0];
				if (matches.Length==1) {
					// Unambiguous
					if (member is FieldInfo)
						mode = Mode.Field;
					else if (member is PropertyInfo)
						mode = Mode.Property;
					else if (member is EventInfo)
						mode = Mode.Event;
					else if (member is MethodInfo)
						mode = Mode.Method;
					else throw new Exception("Unknown member type");
				} else
					mode = Mode.AmbiguousMethod;
			}
		}

		public void  Write(object o, object newValue) {
			restart:
				switch (mode) {
					case Mode.Initialize:
						LookupMemberFor(o);
                        switch (mode) {
                            case Mode.OpenField:
                            case Mode.OpenProperty:
                            case Mode.OpenEvent:
                            case Mode.OpenMethod:
                                throw new MissingFieldException(o.GetType().FullName, memberName);

                            default:
                                break;
                                // all is fine; do nothing
                        }
						goto restart;

						//
						// Instance members
						//
					case Mode.Field:
						if (member.DeclaringType.IsInstanceOfType(o)) {
							FieldInfo f = (FieldInfo)member;
							f.SetValue(o, Utilities.CoerceToType(f.FieldType, newValue));
							return;
						}
						mode = Mode.Initialize;
						goto restart;

					case Mode.Property:
						if (member.DeclaringType.IsInstanceOfType(o)) {
							PropertyInfo p = (PropertyInfo)member;
							p.SetValue(o, Utilities.CoerceToType(p.PropertyType, newValue), null);
							return;
						}
						mode = Mode.Initialize;
						goto restart;

					case Mode.Event:
						if (member.DeclaringType.IsInstanceOfType(o)) {
							EventInfo e = (EventInfo)member;
							e.AddEventHandler(o, DelegateGenerator.AsDelegate(e.EventHandlerType, newValue));
							return;
						}
						mode = Mode.Initialize;
						goto restart;

						//
						// Static members
						//
					case Mode.StaticField:
						if (member.DeclaringType==o) {
							((FieldInfo)member).SetValue(null, newValue);
							return;
						}
						mode = Mode.Initialize;
						goto restart;

					case Mode.StaticProperty:
						if (member.DeclaringType==o) {
							((PropertyInfo)member).SetValue(null, newValue, null);
							return;
						}
						mode = Mode.Initialize;
						goto restart;

					case Mode.StaticEvent:
						if (member.DeclaringType.IsInstanceOfType(o)) {
							EventInfo e = (EventInfo)member;
							e.AddEventHandler(null, DelegateGenerator.AsDelegate(e.EventHandlerType, newValue));
							return;
						}
						mode = Mode.Initialize;
						goto restart;

					default:
						if (member.DeclaringType.IsInstanceOfType(o))
							throw new Exception("Can only assign to members that are fields or properties.");
						mode = Mode.Initialize;
						goto restart;
				}
		}

		public object Read(object o) {
			restart:
				switch (mode) {
					case Mode.Initialize:
						LookupMemberFor(o);
						goto restart;

						//
						// Instance members
						//
					case Mode.Field:
						if (member.DeclaringType.IsInstanceOfType(o))
							return ((FieldInfo)member).GetValue(o);
						mode = Mode.Initialize;
						goto restart;

					case Mode.Property:
						if (member.DeclaringType.IsInstanceOfType(o))
							return ((PropertyInfo)member).GetValue(o, null);
						mode = Mode.Initialize;
						goto restart;

					case Mode.Method:
						if (member.DeclaringType.IsInstanceOfType(o))
							return new ClosedMethodWrapper(memberName, o, (MethodInfo)member);
						mode = Mode.Initialize;
						goto restart;

					case Mode.AmbiguousMethod:
						if (member.DeclaringType.IsInstanceOfType(o))
							return new ClosedAmbiguousMethodWrapper(memberName, o, memberName);
						mode = Mode.Initialize;
						goto restart;

					case Mode.Event:
						if (member.DeclaringType.IsInstanceOfType(o))
							return new Event(o, (EventInfo)member);
						mode = Mode.Initialize;
						goto restart;

						//
						// Static members
						//
					case Mode.StaticField:
						if (member.DeclaringType==o)
							return ((FieldInfo)member).GetValue(null);
						mode = Mode.Initialize;
						goto restart;

					case Mode.StaticProperty:
						if (member.DeclaringType==o)
							return ((PropertyInfo)member).GetValue(null, null);
						mode = Mode.Initialize;
						goto restart;

					case Mode.StaticMethod:
						if (member.DeclaringType==o)
							return new ClosedMethodWrapper(memberName, o, (MethodInfo)member);
						mode = Mode.Initialize;
						goto restart;

					case Mode.AmbiguousStaticMethod:
						if (member.DeclaringType==o)
							return new ClosedAmbiguousStaticMethodWrapper(memberName, member.DeclaringType, memberName);
						mode = Mode.Initialize;
						goto restart;

					case Mode.StaticEvent:
						if (member.DeclaringType==o)
							return new Event(o, (EventInfo)member);
						mode = Mode.Initialize;
						goto restart;

					case Mode.SubType:
						Type t = (Type)member;
						if (t.DeclaringType==o)
							return member;
						mode = Mode.Initialize;
						goto restart;

					case Mode.OpenField:
						return new Meta.FieldWrapper(memberName, (FieldInfo)member);
					case Mode.OpenMethod:
						return new Meta.MethodWrapper(memberName, (MethodInfo)member);
					case Mode.OpenProperty:
						return new Meta.PropertyWrapper(memberName, (PropertyInfo)member);
					case Mode.OpenEvent:
						return new Meta.MethodWrapper(memberName, ((EventInfo)member).GetRaiseMethod(true));
					default:
						throw new Exception("Internal error: invalid mode");
				}
		}

		public object Invoke(object o, object[] args, EvalStack e) {
			object result;
			e.PushMethodCall(o, memberName, args);
			restart:
				switch (mode) {
					case Mode.Initialize:
						LookupMemberFor(o);
						goto restart;

					case Mode.AmbiguousMethod:
						// The semantics of this seem to be that if we say Public | Private, it looks for a methods that's both, and then fails.
						// So for the moment, you can only call public ambiguous non-static methods.
						result = o.GetType().InvokeMember(memberName, BindingFlags.InvokeMethod | BindingFlags.Instance | BindingFlags.Public | BindingFlags.OptionalParamBinding, null, o, args);
						break;
#if INVOKE_HARDWAY
							MethodInfo meth = o.GetType().GetMethod(memberName, Type.GetTypeArray(args));
							if (meth==null)
								throw new MissingMethodException(o.GetType().Name, memberName);
							return meth.Invoke(o, args);
#endif

					case Mode.AmbiguousStaticMethod:
						result = ((Type)o).InvokeMember(memberName, BindingFlags.InvokeMethod | BindingFlags.Static | BindingFlags.Public | BindingFlags.NonPublic | BindingFlags.OptionalParamBinding, null, null, args);
						break;

					case Mode.Method:
						MethodInfo m = (MethodInfo)member;
						if (m.DeclaringType.IsInstanceOfType(o)) {
							result = m.Invoke(o, args);
						} else {
							mode = Mode.Initialize;
							goto restart;
						}
						break;

					case Mode.StaticMethod:
						if (member.DeclaringType==o) {
							MethodInfo mi=(MethodInfo)member;
							result = mi.Invoke(null, args);
						} else {
							mode = Mode.Initialize;
							goto restart;
						}
						break;

					case Mode.Property:
						if (member.DeclaringType.IsInstanceOfType(o))
							result = ((PropertyInfo)member).GetValue(o, args);
						else {
							mode = Mode.Initialize;
							goto restart;
						}
						break;

					case Mode.StaticProperty:
						if (member.DeclaringType==o)
							result = ((PropertyInfo)member).GetValue(null, args);
						else {
							mode = Mode.Initialize;
							goto restart;
						}
						break;

					case Mode.Field:
						if (member.DeclaringType.IsInstanceOfType(o)) {
							object val = ((FieldInfo)member).GetValue(o);
							if (val is Procedure)
								result = Procedure.TracedApply((Procedure)val, args);
							else
								throw new BadProcedureException(val);
						} else {
							mode = Mode.Initialize;
							goto restart;
						}
						break;

					case Mode.StaticField:
						if (member.DeclaringType==o) {
							object val = ((FieldInfo)member).GetValue(null);
							if (val is Procedure)
								result = Procedure.TracedApply((Procedure)val, args);
							else
								throw new BadProcedureException(val);
						} else {
							mode = Mode.Initialize;
							goto restart;
						}
						break;

					default:
						throw new Exception("Specified member is not a method");
				}
			e.Pop();
			return result;
		}
	}

	[Serializable]
	public class SetFieldsProcedure : ApplyInterfaceProcedure {
		public SetFieldsProcedure() {
			this.name = Symbol.Intern("set-fields");
		}

		public override object Apply(object[] args) {
			if (args.Length==0)
				throw new ArgumentCountException(args, new object[1] { Symbol.Intern("object") });
			else if (args.Length%2 != 1)
				throw new Exception("set-fields requires an odd number of arguments");
			object o=args[0];
			Type t=o.GetType();
			for (int i=1; i<args.Length-1;i+=2) {
				object key = args[i];
				Keyword k = key as Keyword;
				if (k==null)
					throw new ArgumentTypeException("Field", key, typeof(Keyword));
				else {
					MemberInfo[] m = t.GetMember(k.NameWithoutColon);
					if (m==null || m.Length==0)
						throw new MissingMemberException(t.Name, k.NameWithoutColon);
					FieldInfo f = m[0] as FieldInfo;
					PropertyInfo p = m[0] as PropertyInfo;
					EventInfo e = m[0] as EventInfo;
					if (f==null) {
						if (p==null) {
							if (e==null)
								throw new MissingFieldException(t.Name, k.NameWithoutColon);
							else
								e.AddEventHandler(o, DelegateGenerator.AsDelegate(e.EventHandlerType, args[i+1]));
						}else
							p.SetValue(o, args[i+1], new object[0]);
					} else
						f.SetValue(o, args[i+1]);
				}
			}
			return o;
		}
	}
}
